Exercises
These exercises are a little bit different. Instead of being given Acceptance Criteria and asked to change the code, these exercises are more like thought experiments. You'll be shown solutions from previous exercises in the course, and asked to consider whether you think they work, in terms of having a single source of truth.
You're welcome to still make changes to the code if you'd like, but the point of the exercises is to evaluate the code. Does it comply with the idea of having a single source of truth? Why or why not?
Counter 2.0 Revisited
Back in Module 2, we wired up a “Counter 2.0”, a souped-up version of a typical Counter
component.
Let's look at our solution through this lens of controlled/uncontrolled components, having a single source of truth. What do you think? Does it follow the rules, or should we make some changes?
Code Playground
My explanation:
Video Summary
I chose this example because several students have actually asked about this, wondering if this is a bad practice! Lots of people have heard that copying props into state is an anti-pattern in React, and yet that's what we're doing here.
We have a single state variable, count
, inside the Counter
component. Because it's managed within the component (and not in the consumer App
component), we could say it's an uncontrolled component that is entirely self-managed… but then we have this initialVal
prop, and this prop does affect the state!
For our single concern (the displayed “count” inside the Counter
), it does sorta seem like it can be set in two different places.
But here's the thing: the prop is initialVal, not value. The word “initial” is important.
Let's suppose that initialVal
was being set to a state variable:
import React from 'react';
import Counter from './Counter';
function App() { const [val, setVal] = React.useState(10);
return ( <> <Counter initialVal={val} />
<button onClick={() => { setVal(Math.random()); }} > Change value </button> </> );}
export default App;
We have a button that randomizes the value of the val
state variable.
Now, let's suppose we click that button. val
is set to a random decimal number like 0.96257135. I don't expect this to affect the UI, however.
By giving the prop a name like initialVal, we're making clear that this prop is only used for the initialized value of the count
state variable.
Here's how I like to think about it: From the very first render to the very last, our text input is being controlled by the count
state variable. That's the source of truth. But that source of truth can be initialized to a value specified by the consumer, and passed in as a prop.
There is a precedent for this. Let's consider an uncontrolled text input:
<input defaultValue="hello world!"/>
The defaultValue
attribute allows us to specify the initial value being used by an uncontrolled input. When the component first mounts, this value is injected into the DOM, and used for the initial value, but it's still 100% an uncontrolled element; it uses internal DOM state, not React state.
The issue with multiple sources of truth is that it's very hard to synchronize between them; it tends to make our code messy and chaotic. In this case, though, there's nothing to synchronize, and so there's no problem.
In React, we're 100% allowed to copy props into state as long as it's clear that we're setting the initial value, and that changes to that prop won't affect the state.
This isn't just my opinion, this is according to the React docs.
Shopping List Revisited
In this exercise, we're taking another look at the “Shopping List” exercise from Module 2.
Once again, consider this code. Is the AddNewItemForm
controlled or uncontrolled? Do we have a single source of truth for every concern? Are we synchronizing between state variables?
Essentially, is this code OK, or should we make some changes so that it conforms better with the ideas we've been discussing?
Code Playground
My explanation:
Video Summary
So the thing that makes this one interesting is that we're sorta “transferring” the source of truth; when the user submits the form, we move the entered value from one place to another:
These two lines are a bit suspicious, aren't they?
<form onSubmit={(event) => { event.preventDefault();
handleAddItem(label); setLabel('') }}>
This looks quite a bit like the “synchronization” that I said was bad in the previous lesson!
Ultimately, though, I don't think that's the right way to look at it. The string "apple" isn't a concern, it's a value.
I'd say this code has two concerns:
- The list of items I need to buy when I go to the grocery store
- The tentative value I'm considering adding to the list
We actually saw a very similar example in the Wordle project. In that project, we had the following code:
// Game.jsfunction Game() { // The array of locked-in guesses const [guesses, setGuesses] = React.useState([]);
// ✂️ Trimmed out other stuff}
// GuessInput.jsfunction GuessInput({ gameStatus, handleSubmitGuess }) { // The hypothetical, theoretical guess: const [tentativeGuess, setTentativeGuess] = React.useState('');
function handleSubmit(event) { event.preventDefault();
handleSubmitGuess(tentativeGuess);
setTentativeGuess(''); }
// ✂️ Trimmed out other stuff}
I think it's even clearer in the Wordle project. GuessInput
is a sketchpad, a place for us to doodle and experiment. While guesses
is an array of locked-in guesses. They're two distinct things.
In our shopping list app, AddNewItemForm
has a controlled <input>
, and that input is always locked to the label
state variable. That never changes.
And so, AddNewItemForm
is an uncontrolled component. It manages its own internal state. And it just so happens to include a callback that fires when the form is submitted. That detail isn't particularly pertinent.
Tying this back to controlled/uncontrolled inputs, it's a bit like this:
<input onChange={doSomethingOnChange}/>
Uncontrolled inputs are totally allowed to have event handlers! As long as we aren't trying to bind it to a value, it's uncontrolled.
Similarly, the AddNewItemForm
component is uncontrolled; we're attaching a handler, but not supplying a value:
<AddNewItemForm handleAddItem={handleAddItem}/>